package server

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"testing"
	"time"

	builderApi "github.com/attestantio/go-builder-client/api"
	builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb"
	builderApiFulu "github.com/attestantio/go-builder-client/api/fulu"
	builderApiV1 "github.com/attestantio/go-builder-client/api/v1"
	builderSpec "github.com/attestantio/go-builder-client/spec"
	eth2Api "github.com/attestantio/go-eth2-client/api"
	eth2ApiV1Bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix"
	eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella"
	eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb"
	eth2ApiV1Electra "github.com/attestantio/go-eth2-client/api/v1/electra"
	"github.com/attestantio/go-eth2-client/spec"
	"github.com/attestantio/go-eth2-client/spec/altair"
	"github.com/attestantio/go-eth2-client/spec/bellatrix"
	"github.com/attestantio/go-eth2-client/spec/capella"
	"github.com/attestantio/go-eth2-client/spec/deneb"
	"github.com/attestantio/go-eth2-client/spec/phase0"
	eth2UtilBellatrix "github.com/attestantio/go-eth2-client/util/bellatrix"
	"github.com/flashbots/mev-boost/common"
	"github.com/flashbots/mev-boost/server/mock"
	"github.com/flashbots/mev-boost/server/params"
	"github.com/flashbots/mev-boost/server/types"
	"github.com/holiman/uint256"
	"github.com/prysmaticlabs/go-bitfield"
	"github.com/stretchr/testify/require"
)

type testBackend struct {
	boost  *BoostService
	relays []*mock.Relay
}

// newTestBackend creates a new backend, initializes mock relays, registers them and return the instance
func newTestBackend(t *testing.T, numRelays int, relayTimeout time.Duration) *testBackend {
	t.Helper()
	backend := testBackend{
		relays: make([]*mock.Relay, numRelays),
	}

	relayConfigs := make([]types.RelayConfig, numRelays)
	for i := 0; i < numRelays; i++ {
		// Create a mock relay
		backend.relays[i] = mock.NewRelay(t)
		relayConfigs[i] = types.NewRelayConfig(backend.relays[i].RelayEntry)
	}

	// setting genesisTime so slot 1 appears to be now (msIntoSlot ≈ 0)
	// most tests use slot 1. Tests using other slots (e.g. for timing games)
	// override genesisTime appropriately.
	genesisTime := uint64(time.Now().Unix()) - 12

	opts := BoostServiceOpts{
		Log:                      mock.TestLog,
		ListenAddr:               "localhost:12345",
		RelayConfigs:             relayConfigs,
		GenesisForkVersionHex:    "0x00000000",
		GenesisTime:              genesisTime,
		RelayCheck:               true,
		RelayMinBid:              types.IntToU256(12345),
		RequestTimeoutGetHeader:  relayTimeout,
		RequestTimeoutGetPayload: relayTimeout,
		RequestTimeoutRegVal:     relayTimeout,
		RequestMaxRetries:        5,
		TimeoutGetHeaderMs:       950,
		LateInSlotTimeMs:         2000, // Realistic production default
	}
	service, err := NewBoostService(opts)
	require.NoError(t, err)

	backend.boost = service
	return &backend
}

func (be *testBackend) request(t *testing.T, method, path string, header http.Header, payload any) *httptest.ResponseRecorder {
	t.Helper()
	var req *http.Request
	var err error

	if payload == nil {
		req, err = http.NewRequest(method, path, bytes.NewReader(nil))
	} else {
		payloadBytes, err2 := json.Marshal(payload)
		require.NoError(t, err2)
		req, err = http.NewRequest(method, path, bytes.NewReader(payloadBytes))
	}
	require.NoError(t, err)

	// Set header
	req.Header = header

	rr := httptest.NewRecorder()
	be.boost.getRouter().ServeHTTP(rr, req)
	return rr
}

func (be *testBackend) requestBytes(t *testing.T, method, path string, header http.Header, payloadBytes []byte) *httptest.ResponseRecorder {
	t.Helper()
	var req *http.Request
	var err error

	req, err = http.NewRequest(method, path, bytes.NewReader(payloadBytes))
	require.NoError(t, err)

	// Set header
	req.Header = header

	rr := httptest.NewRecorder()
	be.boost.getRouter().ServeHTTP(rr, req)
	return rr
}

func TestNewBoostServiceErrors(t *testing.T) {
	t.Run("errors when no relays", func(t *testing.T) {
		_, err := NewBoostService(BoostServiceOpts{
			Log:                      mock.TestLog,
			ListenAddr:               ":123",
			RelayConfigs:             []types.RelayConfig{},
			GenesisForkVersionHex:    "0x00000000",
			GenesisTime:              0,
			RelayCheck:               true,
			RelayMinBid:              types.IntToU256(0),
			RequestTimeoutGetHeader:  time.Second,
			RequestTimeoutGetPayload: time.Second,
			RequestTimeoutRegVal:     time.Second,
			RequestMaxRetries:        1,
		})
		require.Error(t, err)
	})
}

func TestWebserver(t *testing.T) {
	t.Run("errors when webserver is already existing", func(t *testing.T) {
		backend := newTestBackend(t, 1, time.Second)
		backend.boost.srv = &http.Server{}
		err := backend.boost.StartHTTPServer()
		require.Error(t, err)
	})

	t.Run("webserver error on invalid listenAddr", func(t *testing.T) {
		backend := newTestBackend(t, 1, time.Second)
		backend.boost.listenAddr = "localhost:876543"
		err := backend.boost.StartHTTPServer()
		require.Error(t, err)
	})

	// t.Run("webserver starts normally", func(t *testing.T) {
	// 	backend := newTestBackend(t, 1, time.Second)
	// 	go func() {
	// 		err := backend.boost.StartHTTPServer()
	// 		require.NoError(t, err)
	// 	}()
	// 	time.Sleep(time.Millisecond * 100)
	// 	backend.boost.srv.Close()
	// })
}

func TestWebserverRootHandler(t *testing.T) {
	backend := newTestBackend(t, 1, time.Second)

	// Check root handler
	req, _ := http.NewRequest(http.MethodGet, "/", nil)
	rr := httptest.NewRecorder()
	backend.boost.getRouter().ServeHTTP(rr, req)
	require.Equal(t, http.StatusOK, rr.Code)
	require.Equal(t, "{}\n", rr.Body.String())
}

func TestWebserverMaxHeaderSize(t *testing.T) {
	backend := newTestBackend(t, 1, time.Second)
	addr := "localhost:1234"
	backend.boost.listenAddr = addr
	go func() {
		err := backend.boost.StartHTTPServer()
		require.NoError(t, err) //nolint:testifylint
	}()
	time.Sleep(time.Millisecond * 100)
	path := "http://" + addr + "?" + strings.Repeat("abc", 4000) // path with characters of size over 4kb
	code, err := SendHTTPRequest(t.Context(), *http.DefaultClient, http.MethodGet, path, "test", nil, nil, nil)
	require.Error(t, err)
	require.Equal(t, http.StatusRequestHeaderFieldsTooLarge, code)
	backend.boost.srv.Close()
}

func TestStatus(t *testing.T) {
	t.Run("At least one relay is available", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		time.Sleep(time.Millisecond * 20)
		path := "/eth/v1/builder/status"
		rr := backend.request(t, http.MethodGet, path, header, nil)

		require.Equal(t, http.StatusOK, rr.Code)
		require.NotEmpty(t, rr.Header().Get("X-MEVBoost-Version"))
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
	})

	t.Run("No relays available", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		backend.relays[0].Server.Close() // makes the relay unavailable

		path := "/eth/v1/builder/status"
		rr := backend.request(t, http.MethodGet, path, header, nil)

		require.Equal(t, http.StatusServiceUnavailable, rr.Code)
		require.NotEmpty(t, rr.Header().Get("X-MEVBoost-Version"))
		require.Equal(t, 0, backend.relays[0].GetRequestCount(path))
	})
}

func TestRegisterValidator(t *testing.T) {
	path := "/eth/v1/builder/validators"
	reg := builderApiV1.SignedValidatorRegistration{
		Message: &builderApiV1.ValidatorRegistration{
			FeeRecipient: mock.HexToAddress("0xdb65fEd33dc262Fe09D9a2Ba8F80b329BA25f941"),
			Timestamp:    time.Unix(1234356, 0),
			Pubkey: mock.HexToPubkey(
				"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249"),
		},
		Signature: mock.HexToSignature(
			"0x81510b571e22f89d1697545aac01c9ad0c1e7a3e778b3078bef524efae14990e58a6e960a152abd49de2e18d7fd3081c15d5c25867ccfad3d47beef6b39ac24b6b9fbf2cfa91c88f67aff750438a6841ec9e4a06a94ae41410c4f97b75ab284c"),
	}
	payload := []builderApiV1.SignedValidatorRegistration{reg}

	t.Run("Normal function", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusOK, rr.Code)
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Relay error response", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 2, time.Second)
		backend.relays[0].ResponseDelay = 5 * time.Millisecond
		backend.relays[1].ResponseDelay = 5 * time.Millisecond

		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusOK, rr.Code)
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[1].GetRequestCount(path))

		// Now make one relay return an error
		backend.relays[0].OverrideHandleRegisterValidator(func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusBadRequest)
		})

		rr = backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusOK, rr.Code)
		require.Equal(t, 2, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 2, backend.relays[1].GetRequestCount(path))

		// Now make both relays return an error - which should cause the request to fail
		backend.relays[1].OverrideHandleRegisterValidator(func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusBadRequest)
		})
		rr = backend.request(t, http.MethodPost, path, header, payload)
		require.JSONEq(t, `{"code":502,"message":"no successful relay response"}`+"\n", rr.Body.String())
		require.Equal(t, http.StatusBadGateway, rr.Code)
		require.Equal(t, 3, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 3, backend.relays[1].GetRequestCount(path))
	})

	t.Run("mev-boost relay timeout works with slow relay", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, 150*time.Millisecond) // 10ms max
		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusOK, rr.Code)

		// Now make the relay return slowly, mev-boost should return an error
		backend.relays[0].ResponseDelay = 180 * time.Millisecond
		rr = backend.request(t, http.MethodPost, path, header, payload)
		require.JSONEq(t, `{"code":502,"message":"no successful relay response"}`+"\n", rr.Body.String())
		require.Equal(t, http.StatusBadGateway, rr.Code)
		require.Equal(t, 2, backend.relays[0].GetRequestCount(path))
	})
}

func getHeaderPath(slot uint64, parentHash phase0.Hash32, pubkey phase0.BLSPubKey) string {
	return fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", slot, parentHash.String(), pubkey.String())
}

func TestGetHeader(t *testing.T) {
	hash := mock.HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7")
	pubkey := mock.HexToPubkey(
		"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249")
	path := getHeaderPath(1, hash, pubkey)
	require.Equal(t, "/eth/v1/builder/header/1/0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7/0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", path)

	t.Run("Okay response from relay", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Okay response from relay deneb", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Okay response from relay deneb in ssz", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderEthConsensusVersion, "deneb")
		header.Set(HeaderAccept, MediaTypeOctetStream)

		backend := newTestBackend(t, 1, time.Second)
		resp := backend.relays[0].MakeGetHeaderResponse(
			12345,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)
		backend.relays[0].GetHeaderResponse = resp
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType))

		// Ensure the response was SSZ
		bid := new(builderApiDeneb.SignedBuilderBid)
		err := bid.UnmarshalSSZ(rr.Body.Bytes())
		require.NoError(t, err)
		require.Equal(t, *resp.Deneb, *bid)
	})

	t.Run("Relay returns SSZ, mev-boost returns SSZ", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderEthConsensusVersion, "deneb")
		header.Set(HeaderAccept, "application/octet-stream;q=1.0,application/json;q=0.9")

		backend := newTestBackend(t, 1, time.Second)
		backend.relays[0].ForceSSZ = true
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType))
	})

	t.Run("One relay returns SSZ, another relay returns JSON, mev-boost returns SSZ", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderEthConsensusVersion, "deneb")
		header.Set(HeaderAccept, "application/octet-stream;q=1.0,application/json;q=0.9")

		backend := newTestBackend(t, 2, time.Second)
		backend.relays[0].ForceSSZ = true
		backend.relays[1].ForceJSON = true
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
		require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType))
	})

	t.Run("One relay returns JSON, mev-boost returns preferred SSZ", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderEthConsensusVersion, "deneb")
		header.Set(HeaderAccept, "application/octet-stream;q=1.0,application/json;q=0.9")

		backend := newTestBackend(t, 1, time.Second)
		backend.relays[0].ForceJSON = true
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType))
	})

	t.Run("Two relays return JSON, mev-boost returns preferred SSZ", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderEthConsensusVersion, "deneb")
		header.Set(HeaderAccept, "application/octet-stream;q=1.0,application/json;q=0.9")

		backend := newTestBackend(t, 2, time.Second)
		backend.relays[0].ForceJSON = true
		backend.relays[1].ForceJSON = true
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
		require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType))
	})

	t.Run("Accepts both with Q values", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderEthConsensusVersion, "deneb")
		header.Set(HeaderAccept, "application/octet-stream;q=1.0,application/json;q=0.9")

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType))
	})

	t.Run("No accept value", func(t *testing.T) {
		// This should default to JSON
		header := make(http.Header)
		header.Set(HeaderEthConsensusVersion, "deneb")
		header.Del(HeaderAccept)

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, MediaTypeJSON, rr.Header().Get(HeaderContentType)) //nolint:testifylint
	})

	t.Run("Accepts both but prefers JSON", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderEthConsensusVersion, "deneb")
		header.Set(HeaderAccept, "application/octet-stream;q=0.9,application/json;q=1.0")

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, MediaTypeJSON, rr.Header().Get(HeaderContentType)) //nolint:testifylint
	})

	t.Run("Only accepts unsupported media type", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderEthConsensusVersion, "deneb")
		header.Set(HeaderAccept, "plain/text")

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusNotAcceptable, rr.Code, rr.Body.String())
	})

	t.Run("Bad response from relays", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 2, time.Second)
		resp := backend.relays[0].MakeGetHeaderResponse(
			12345,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)
		resp.Deneb.Message.Header.BlockHash = nilHash

		// 1/2 failing responses are okay
		backend.relays[0].GetHeaderResponse = resp
		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		// 2/2 failing responses are okay
		backend.relays[1].GetHeaderResponse = resp
		rr = backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, 2, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 2, backend.relays[1].GetRequestCount(path))
		require.Equal(t, http.StatusNoContent, rr.Code)
	})

	t.Run("Invalid relay public key", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)

		backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse(
			12345,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		// Simulate a different public key registered to mev-boost
		pk := phase0.BLSPubKey{}
		backend.boost.relayConfigs[0].RelayEntry.PublicKey = pk

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))

		// Request should have no content
		require.Equal(t, http.StatusNoContent, rr.Code)
	})

	t.Run("Invalid relay signature", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)

		backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse(
			12345,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		// Scramble the signature
		backend.relays[0].GetHeaderResponse.Deneb.Signature = phase0.BLSSignature{}

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))

		// Request should have no content
		require.Equal(t, http.StatusNoContent, rr.Code)
	})

	t.Run("Invalid slot number", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		// Number larger than uint64 creates parsing error
		slot := fmt.Sprintf("%d0", uint64(math.MaxUint64))
		invalidSlotPath := fmt.Sprintf("/eth/v1/builder/header/%s/%s/%s", slot, hash.String(), pubkey.String())

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodGet, invalidSlotPath, header, nil)
		require.JSONEq(t, `{"code":400,"message":"invalid slot"}`+"\n", rr.Body.String())
		require.Equal(t, http.StatusBadRequest, rr.Code, rr.Body.String())
		require.Equal(t, 0, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Invalid pubkey length", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		invalidPubkeyPath := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 1, hash.String(), "0x1")

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodGet, invalidPubkeyPath, header, nil)
		require.JSONEq(t, `{"code":400,"message":"invalid pubkey"}`+"\n", rr.Body.String())
		require.Equal(t, http.StatusBadRequest, rr.Code, rr.Body.String())
		require.Equal(t, 0, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Invalid hash length", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		invalidSlotPath := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 1, "0x1", pubkey.String())

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodGet, invalidSlotPath, header, nil)
		require.JSONEq(t, `{"code":400,"message":"invalid hash"}`+"\n", rr.Body.String())
		require.Equal(t, http.StatusBadRequest, rr.Code, rr.Body.String())
		require.Equal(t, 0, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Invalid parent hash", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)

		invalidParentHashPath := getHeaderPath(1, phase0.Hash32{}, pubkey)
		rr := backend.request(t, http.MethodGet, invalidParentHashPath, header, nil)
		require.Equal(t, http.StatusNoContent, rr.Code)
		require.Equal(t, 0, backend.relays[0].GetRequestCount(path))
	})
}

func TestGetHeaderBids(t *testing.T) {
	hash := mock.HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7")
	pubkey := mock.HexToPubkey(
		"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249")
	path := getHeaderPath(2, hash, pubkey)
	require.Equal(t, "/eth/v1/builder/header/2/0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7/0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", path)

	t.Run("Use header with highest value", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		// Create backend and register 3 relays.
		backend := newTestBackend(t, 3, time.Second)
		// setting genesisTime to make slot 2 appear now
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 24

		// First relay will return signed response with value 12345.
		backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse(
			12345,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		// First relay will return signed response with value 12347.
		backend.relays[1].GetHeaderResponse = backend.relays[1].MakeGetHeaderResponse(
			12347,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		// First relay will return signed response with value 12346.
		backend.relays[2].GetHeaderResponse = backend.relays[2].MakeGetHeaderResponse(
			12346,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		// Run the request.
		rr := backend.request(t, http.MethodGet, path, header, nil)

		// Each relay must have received the request.
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[2].GetRequestCount(path))

		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		// Highest value should be 12347, i.e. second relay.
		resp := new(builderSpec.VersionedSignedBuilderBid)
		err := json.Unmarshal(rr.Body.Bytes(), resp)
		require.NoError(t, err)
		value, err := resp.Value()
		require.NoError(t, err)
		require.Equal(t, uint256.NewInt(12347), value)
	})

	t.Run("Use header with lowest blockhash if same value", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		// Create backend and register 3 relays.
		backend := newTestBackend(t, 3, time.Second)
		// setting genesisTime to make slot 2 appear now
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 24

		backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse(
			12345,
			"0xa38385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		backend.relays[1].GetHeaderResponse = backend.relays[1].MakeGetHeaderResponse(
			12345,
			"0xa18385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		backend.relays[2].GetHeaderResponse = backend.relays[2].MakeGetHeaderResponse(
			12345,
			"0xa28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		// Run the request.
		rr := backend.request(t, http.MethodGet, path, header, nil)

		// Each relay must have received the request.
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[2].GetRequestCount(path))

		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		// Highest value should be 12347, i.e. second relay.
		resp := new(builderSpec.VersionedSignedBuilderBid)

		err := json.Unmarshal(rr.Body.Bytes(), resp)
		require.NoError(t, err)
		value, err := resp.Value()
		require.NoError(t, err)
		require.Equal(t, uint256.NewInt(12345), value)
		blockHash, err := resp.BlockHash()
		require.NoError(t, err)
		require.Equal(t, "0xa18385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", blockHash.String())
	})

	t.Run("Respect minimum bid cutoff", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		// Create backend and register relay.
		backend := newTestBackend(t, 1, time.Second)
		// setting genesisTime to make slot 2 appear now
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 24

		// Relay will return signed response with value 12344.
		backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse(
			12344,
			"0xa28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		// Run the request.
		rr := backend.request(t, http.MethodGet, path, header, nil)

		// Each relay must have received the request.
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))

		// Request should have no content (min bid is 12345)
		require.Equal(t, http.StatusNoContent, rr.Code)
	})

	t.Run("Allow bids which meet minimum bid cutoff", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		// Create backend and register relay.
		backend := newTestBackend(t, 1, time.Second)
		// setting genesisTime to make slot 2 appear now
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 24

		// First relay will return signed response with value 12345.
		backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse(
			12345,
			"0xa28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		// Run the request.
		rr := backend.request(t, http.MethodGet, path, header, nil)

		// Each relay must have received the request.
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))

		// Value should be 12345 (min bid is 12345)
		resp := new(builderSpec.VersionedSignedBuilderBid)
		err := json.Unmarshal(rr.Body.Bytes(), resp)
		require.NoError(t, err)
		value, err := resp.Value()
		require.NoError(t, err)
		require.Equal(t, uint256.NewInt(12345), value)
	})
}

func TestGetHeaderTimingGames(t *testing.T) {
	hash := mock.HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7")
	pubkey := mock.HexToPubkey(
		"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249")
	path := getHeaderPath(3, hash, pubkey)

	t.Run("Relay with timing games sends multiple requests", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)

		// setting genesis time so slot 3 appears to be now (msIntoSlot ≈ 0)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 0
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 50 // request every 50ms

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		// should have received multiple requests due to timing games
		requestCount := backend.relays[0].GetRequestCount(path)
		require.Greater(t, requestCount, 1)
	})

	t.Run("Relay with timing games delays first request", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 100 // wait 100ms from slot start
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 0   // no multiple requests

		rr := backend.request(t, http.MethodGet, path, header, nil)

		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		// with no frequency, should only send one request
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Mix of timing games and normal relays", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 3, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		// timing games enabled for only first relay
		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 0
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 30

		// second relay: without timing games enabled
		backend.relays[1].GetHeaderResponse = backend.relays[1].MakeGetHeaderResponse(
			12346,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		// third relay: without timing games enabled
		backend.relays[2].GetHeaderResponse = backend.relays[2].MakeGetHeaderResponse(
			12347,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		// relay1 should have received more then 1 request due to timing games
		require.Greater(t, backend.relays[0].GetRequestCount(path), 1)
		// relay 2 and 3 should have received exactly one request
		require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[2].GetRequestCount(path))

		resp := new(builderSpec.VersionedSignedBuilderBid)
		err := json.Unmarshal(rr.Body.Bytes(), resp)
		require.NoError(t, err)
		value, err := resp.Value()
		require.NoError(t, err)
		require.Equal(t, uint256.NewInt(12347), value)
	})

	t.Run("Timing games relay with higher bid wins", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 2, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		// relay1: timing games with higher bid
		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 0
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 40
		backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse(
			12350,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		// relay2: normal with lower bid
		backend.relays[1].GetHeaderResponse = backend.relays[1].MakeGetHeaderResponse(
			12348,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		// relay1's bid should win
		resp := new(builderSpec.VersionedSignedBuilderBid)
		err := json.Unmarshal(rr.Body.Bytes(), resp)
		require.NoError(t, err)
		value, err := resp.Value()
		require.NoError(t, err)
		require.Equal(t, uint256.NewInt(12350), value)
	})

	t.Run("Timing games with SSZ encoding", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderEthConsensusVersion, "deneb")
		header.Set(HeaderAccept, MediaTypeOctetStream)

		backend := newTestBackend(t, 1, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36
		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 0
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 50

		backend.relays[0].ForceSSZ = true
		backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse(
			12345,
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Greater(t, backend.relays[0].GetRequestCount(path), 1)
		require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType))

		bid := new(builderApiDeneb.SignedBuilderBid)
		err := bid.UnmarshalSSZ(rr.Body.Bytes())
		require.NoError(t, err)
	})

	t.Run("Timing games respects timeout budget", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		backend.boost.timeoutGetHeaderMs = 100
		backend.boost.lateInSlotTimeMs = 2000

		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 0
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 20

		start := time.Now()
		rr := backend.request(t, http.MethodGet, path, header, nil)
		elapsed := time.Since(start)

		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.LessOrEqual(t, elapsed.Milliseconds(), int64(300))

		requestCount := backend.relays[0].GetRequestCount(path)
		require.Greater(t, requestCount, 1)
		require.LessOrEqual(t, requestCount, 5) // at most 100ms / 20ms = 5 requests
	})

	t.Run("Multiple timing games relays compete", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 2, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		// both relays use timing games
		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 0
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 40
		backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse(
			12345,
			"0xa18385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		backend.boost.relayConfigs[1].EnableTimingGames = true
		backend.boost.relayConfigs[1].TargetFirstRequestMs = 0
		backend.boost.relayConfigs[1].FrequencyGetHeaderMs = 35
		backend.relays[1].GetHeaderResponse = backend.relays[1].MakeGetHeaderResponse(
			12345,
			"0xa28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
			"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
			spec.DataVersionDeneb,
		)

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		require.Greater(t, backend.relays[0].GetRequestCount(path), 1)
		require.Greater(t, backend.relays[1].GetRequestCount(path), 1)

		resp := new(builderSpec.VersionedSignedBuilderBid)
		err := json.Unmarshal(rr.Body.Bytes(), resp)
		require.NoError(t, err)
		blockHash, err := resp.BlockHash()
		require.NoError(t, err)
		require.Equal(t, "0xa18385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", blockHash.String())
	})

	t.Run("Higher bid received on later request wins", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		// enable timing games for relay1
		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 0
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 50 // request every 50ms

		requestCount := 0

		backend.relays[0].OverrideHandleGetHeader(func(w http.ResponseWriter, _ *http.Request) {
			requestCount++

			var resp *builderSpec.VersionedSignedBuilderBid
			switch requestCount {
			case 1:
				// first request: lower bid
				resp = backend.relays[0].MakeGetHeaderResponse(
					12345,
					"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
					"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
					"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
					spec.DataVersionDeneb,
				)
			case 2:
				// second request: medium bid
				resp = backend.relays[0].MakeGetHeaderResponse(
					12400,
					"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
					"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
					"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
					spec.DataVersionDeneb,
				)
			default:
				resp = backend.relays[0].MakeGetHeaderResponse(
					12500,
					"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
					"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
					"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
					spec.DataVersionDeneb,
				)
			}
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			if err := json.NewEncoder(w).Encode(resp); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			}
		})

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		finalCount := backend.relays[0].GetRequestCount(path)
		require.Greater(t, finalCount, 2)

		// should select the highest bid from the later request
		bidResp := new(builderSpec.VersionedSignedBuilderBid)
		err := json.Unmarshal(rr.Body.Bytes(), bidResp)
		require.NoError(t, err)
		value, err := bidResp.Value()
		require.NoError(t, err)
		require.Equal(t, uint256.NewInt(12500), value)
	})

	t.Run("Timing games with delayed first request and frequency", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		// expected: 200ms / 50ms = 4 requests
		backend.boost.timeoutGetHeaderMs = 200
		backend.boost.lateInSlotTimeMs = 2000

		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 0
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 50

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		requestCount := backend.relays[0].GetRequestCount(path)
		require.Equal(t, 4, requestCount)
	})

	t.Run("Timing games disabled sends single request", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		backend.boost.relayConfigs[0].EnableTimingGames = false
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 200 // ignored
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 100 // ignored

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Timing games with zero frequency sends single request", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 50
		// no repeated requests
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 0

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Timing games budget limited by lateInSlotTimeMs", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 36

		backend.boost.timeoutGetHeaderMs = 2000
		backend.boost.lateInSlotTimeMs = 1500

		backend.boost.relayConfigs[0].EnableTimingGames = true
		backend.boost.relayConfigs[0].TargetFirstRequestMs = 0
		backend.boost.relayConfigs[0].FrequencyGetHeaderMs = 100

		rr := backend.request(t, http.MethodGet, path, header, nil)

		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Greater(t, backend.relays[0].GetRequestCount(path), 1)
	})
}

func TestGetHeaderSlotTiming(t *testing.T) {
	hash := mock.HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7")
	pubkey := mock.HexToPubkey(
		"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249")
	path := getHeaderPath(1, hash, pubkey)

	t.Run("Request early in slot succeeds (msIntoSlot < lateInSlotTimeMs)", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		// set genesisTime so slot 1 is now
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 12
		backend.boost.lateInSlotTimeMs = 2000

		rr := backend.request(t, http.MethodGet, path, header, nil)

		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Request late in slot returns no content (msIntoSlot >= lateInSlotTimeMs)", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		// set genesisTime so slot 1 started 5 seconds ago (msIntoSlot ≈ 5000ms)
		// genesisTime = currentTimeSec - 12 - 5 = makes slot 1 start 5 seconds ago
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 12 - 5
		backend.boost.lateInSlotTimeMs = 2000

		rr := backend.request(t, http.MethodGet, path, header, nil)

		require.Equal(t, http.StatusNoContent, rr.Code)
		// no requests should be sent to relays because we return early
		require.Equal(t, 0, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Request at lateInSlotTimeMs boundary returns no content", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		// setting genesisTime so slot 1 started exactly 2 seconds ago
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 12 - 2
		backend.boost.lateInSlotTimeMs = 2000

		rr := backend.request(t, http.MethodGet, path, header, nil)
		require.Equal(t, http.StatusNoContent, rr.Code)
		require.Equal(t, 0, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Request just before lateInSlotTimeMs threshold succeeds", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		// setting genesisTime so slot 1 started 1 second ago (msIntoSlot ≈ 1000ms)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 12 - 1
		backend.boost.lateInSlotTimeMs = 2000

		rr := backend.request(t, http.MethodGet, path, header, nil)

		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Different lateInSlotTimeMs values work correctly", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		// setting genesisTime so slot 1 started 1 second ago (msIntoSlot ≈ 1000ms)
		backend.boost.genesisTime = uint64(time.Now().Unix()) - 12 - 1
		backend.boost.lateInSlotTimeMs = 500

		rr := backend.request(t, http.MethodGet, path, header, nil)

		// should return 204 because msIntoSlot (≈1000ms) >= lateInSlotTimeMs (500ms)
		require.Equal(t, http.StatusNoContent, rr.Code)
	})
}

func TestGetPayload(t *testing.T) {
	path := params.PathGetPayload
	blockHash := mock.HexToHash("0x534809bd2b6832edff8d8ce4cb0e50068804fd1ef432c8362ad708a74fdc0e46")
	payload := &eth2ApiV1Deneb.SignedBlindedBeaconBlock{
		Signature: mock.HexToSignature(
			"0x8c795f751f812eabbabdee85100a06730a9904a4b53eedaa7f546fe0e23cd75125e293c6b0d007aa68a9da4441929d16072668abb4323bb04ac81862907357e09271fe414147b3669509d91d8ffae2ec9c789a5fcd4519629b8f2c7de8d0cce9"),
		Message: &eth2ApiV1Deneb.BlindedBeaconBlock{
			Slot:          1,
			ProposerIndex: 1,
			ParentRoot:    phase0.Root{0x01},
			StateRoot:     phase0.Root{0x02},
			Body: &eth2ApiV1Deneb.BlindedBeaconBlockBody{
				RANDAOReveal: phase0.BLSSignature{0xa1},
				ETH1Data: &phase0.ETH1Data{
					BlockHash: blockHash[:],
				},
				Graffiti: phase0.Hash32{0xa2},
				SyncAggregate: &altair.SyncAggregate{
					SyncCommitteeBits: bitfield.NewBitvector512(),
				},
				ProposerSlashings: []*phase0.ProposerSlashing{},
				AttesterSlashings: []*phase0.AttesterSlashing{},
				Attestations:      []*phase0.Attestation{},
				Deposits:          []*phase0.Deposit{},
				VoluntaryExits:    []*phase0.SignedVoluntaryExit{},
				ExecutionPayloadHeader: &deneb.ExecutionPayloadHeader{
					ParentHash:    mock.HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7"),
					BlockHash:     blockHash,
					BlockNumber:   12345,
					FeeRecipient:  mock.HexToAddress("0xdb65fEd33dc262Fe09D9a2Ba8F80b329BA25f941"),
					BaseFeePerGas: uint256.NewInt(100),
				},
			},
		},
	}

	t.Run("Okay response from relay in JSON", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)
		header.Set("Eth-Consensus-Version", "deneb")

		backend := newTestBackend(t, 1, time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))

		resp := new(builderApi.VersionedSubmitBlindedBlockResponse)
		err := json.Unmarshal(rr.Body.Bytes(), resp)
		require.NoError(t, err)
		require.Equal(t, payload.Message.Body.ExecutionPayloadHeader.BlockHash, resp.Deneb.ExecutionPayload.BlockHash)
	})

	t.Run("Okay response from relay in SSZ", func(t *testing.T) {
		header := make(http.Header)
		header.Set("Eth-Consensus-Version", "deneb")
		header.Set("Accept", "application/octet-stream")

		backend := newTestBackend(t, 1, time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))

		resp := new(builderApi.VersionedSubmitBlindedBlockResponse)
		resp.Deneb = new(builderApiDeneb.ExecutionPayloadAndBlobsBundle)
		err := resp.Deneb.UnmarshalSSZ(rr.Body.Bytes())
		require.NoError(t, err)
		require.Equal(t, payload.Message.Body.ExecutionPayloadHeader.BlockHash, resp.Deneb.ExecutionPayload.BlockHash)
	})

	t.Run("Okay response from relay in SSZ but returns JSON", func(t *testing.T) {
		header := make(http.Header)
		header.Set("Eth-Consensus-Version", "deneb")
		header.Set("Accept", "application/octet-stream")
		header.Set("Content-Type", "application/octet-stream")

		backend := newTestBackend(t, 1, time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		// Tell mev-boost that the relay does not support SSZ
		bid.relays[0].SupportsSSZ = false

		// Only respond with JSON
		backend.relays[0].OverrideHandleGetPayload(func(w http.ResponseWriter, req *http.Request) {
			// Ensure the request was JSON
			reqBody, err := io.ReadAll(req.Body)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			var block eth2ApiV1Deneb.SignedBlindedBeaconBlock
			err = json.Unmarshal(reqBody, &block)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			// Compare HTR as a sanity check
			payloadHTR, err := payload.HashTreeRoot()
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			blockHTR, err := block.HashTreeRoot()
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			if !bytes.Equal(payloadHTR[:], blockHTR[:]) {
				http.Error(w, "htr did not match", http.StatusInternalServerError)
				return
			}

			response := backend.relays[0].MakeGetPayloadResponse(
				"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
				"0x534809bd2b6832edff8d8ce4cb0e50068804fd1ef432c8362ad708a74fdc0e46",
				"0xdb65fEd33dc262Fe09D9a2Ba8F80b329BA25f941",
				12345,
				spec.DataVersionDeneb,
			)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			if err := json.NewEncoder(w).Encode(response); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})

		payloadBytes, err := payload.MarshalSSZ()
		require.NoError(t, err)
		rr := backend.requestBytes(t, http.MethodPost, path, header, payloadBytes)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Convert SSZ request to JSON for relay without SSZ support", func(t *testing.T) {
		header := make(http.Header)
		header.Set("Eth-Consensus-Version", "deneb")
		header.Set("Accept", "application/octet-stream;q=1.0,application/json;q=0.9")
		header.Set("Content-Type", "application/octet-stream")

		backend := newTestBackend(t, 1, time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		// Tell mev-boost that the relay does not support SSZ
		bid.relays[0].SupportsSSZ = false

		backend.relays[0].OverrideHandleGetPayload(func(w http.ResponseWriter, req *http.Request) {
			// Require that the request was JSON
			reqBody, err := io.ReadAll(req.Body)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			var block eth2ApiV1Deneb.SignedBlindedBeaconBlock
			err = json.Unmarshal(reqBody, &block)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			// Require that the Content-Type header of the request was JSON
			reqContentType := req.Header.Get(HeaderContentType)
			require.Equal(t, MediaTypeJSON, reqContentType) //nolint:testifylint // allow require in HTTP handler

			response := backend.relays[0].MakeGetPayloadResponse(
				"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
				"0x534809bd2b6832edff8d8ce4cb0e50068804fd1ef432c8362ad708a74fdc0e46",
				"0xdb65fEd33dc262Fe09D9a2Ba8F80b329BA25f941",
				12345,
				spec.DataVersionDeneb,
			)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			if err := json.NewEncoder(w).Encode(response); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})

		payloadBytes, err := payload.MarshalSSZ()
		require.NoError(t, err)
		rr := backend.requestBytes(t, http.MethodPost, path, header, payloadBytes)

		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))

		require.Equal(t, MediaTypeOctetStream, rr.Header().Get(HeaderContentType))
	})

	t.Run("A relay which does not provide the bid gets a JSON request", func(t *testing.T) {
		header := make(http.Header)
		header.Set("Eth-Consensus-Version", "deneb")
		header.Set("Accept", "application/octet-stream")
		header.Set("Content-Type", "application/octet-stream")

		// Setup backend with 2 relays
		backend := newTestBackend(t, 2, time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, 1)}
		bid.relays[0] = backend.relays[0].RelayEntry
		bid.relays[0].SupportsSSZ = true
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		// Make a wait group to ensure both relays get the request
		var wg sync.WaitGroup
		wg.Add(len(backend.relays))

		// Ensure the first relay gets the request in SSZ
		backend.relays[0].OverrideHandleGetPayload(func(w http.ResponseWriter, req *http.Request) {
			defer wg.Done()
			require.Equal(t, MediaTypeOctetStream, req.Header.Get(HeaderContentType)) //nolint:testifylint
			backend.relays[0].DefaultHandleGetPayload(w, req)
		})

		// Ensure the second relay gets the request in JSON
		backend.relays[1].OverrideHandleGetPayload(func(w http.ResponseWriter, req *http.Request) {
			defer wg.Done()
			require.Equal(t, MediaTypeJSON, req.Header.Get(HeaderContentType)) //nolint:testifylint
			backend.relays[1].DefaultHandleGetPayload(w, req)
		})

		// Send the request
		payloadBytes, err := payload.MarshalSSZ()
		require.NoError(t, err)
		rr := backend.requestBytes(t, http.MethodPost, path, header, payloadBytes)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		// Ensure both relays got the request
		wg.Wait()
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
	})

	t.Run("Bad response from relays", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)
		header.Set("Eth-Consensus-Version", "deneb")

		backend := newTestBackend(t, 2, time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		resp := &builderApi.VersionedSubmitBlindedBlockResponse{
			Version: spec.DataVersionDeneb,
			Deneb: &builderApiDeneb.ExecutionPayloadAndBlobsBundle{
				ExecutionPayload: &deneb.ExecutionPayload{
					BaseFeePerGas: uint256.NewInt(0),
				},
				BlobsBundle: &builderApiDeneb.BlobsBundle{},
			},
		}

		// 1/2 failing responses are okay
		backend.relays[0].GetPayloadResponse = resp
		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.GreaterOrEqual(t, backend.relays[1].GetRequestCount(path)+backend.relays[0].GetRequestCount(path), 1)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

		// 2/2 failing responses are okay
		backend = newTestBackend(t, 2, time.Second)

		// Add the bid to the service
		bid = bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		backend.relays[0].GetPayloadResponse = resp
		backend.relays[1].GetPayloadResponse = resp
		rr = backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
		require.JSONEq(t, `{"code":502,"message":"no successful relay response"}`+"\n", rr.Body.String())
		require.Equal(t, http.StatusBadGateway, rr.Code, rr.Body.String())
	})

	t.Run("Retries on error from relay", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)
		header.Set("Eth-Consensus-Version", "deneb")

		backend := newTestBackend(t, 1, 2*time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		count := 0
		backend.relays[0].OverrideHandleGetPayload(func(w http.ResponseWriter, req *http.Request) {
			if count > 0 {
				// success response on the second attempt
				backend.relays[0].DefaultHandleGetPayload(w, req)
			} else {
				w.WriteHeader(http.StatusInternalServerError)
				_, err := w.Write([]byte(`{"code":500,"message":"internal server error"}`))
				require.NoError(t, err, "failed to write error response") //nolint:testifylint // if we fail here the test is compromised
			}
			count++
		})
		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
	})

	t.Run("Error after max retries are reached", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)
		header.Set("Eth-Consensus-Version", "deneb")

		backend := newTestBackend(t, 1, time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		count := 0
		maxRetries := 5

		backend.relays[0].OverrideHandleGetPayload(func(w http.ResponseWriter, req *http.Request) {
			count++
			if count > maxRetries {
				// success response after max retry attempts
				backend.relays[0].DefaultHandleGetPayload(w, req)
			} else {
				w.WriteHeader(http.StatusInternalServerError)
				_, err := w.Write([]byte(`{"code":500,"message":"internal server error"}`))
				require.NoError(t, err, "failed to write error response") //nolint:testifylint // if we fail here the test is compromised
			}
		})
		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, 5, backend.relays[0].GetRequestCount(path))
		require.JSONEq(t, `{"code":502,"message":"no successful relay response"}`+"\n", rr.Body.String())
		require.Equal(t, http.StatusBadGateway, rr.Code, rr.Body.String())
	})
}

func TestGetPayloadV2(t *testing.T) {
	path := params.PathGetPayloadV2
	blockHash := mock.HexToHash("0x534809bd2b6832edff8d8ce4cb0e50068804fd1ef432c8362ad708a74fdc0e46")
	payload := &eth2ApiV1Deneb.SignedBlindedBeaconBlock{
		Signature: mock.HexToSignature(
			"0x8c795f751f812eabbabdee85100a06730a9904a4b53eedaa7f546fe0e23cd75125e293c6b0d007aa68a9da4441929d16072668abb4323bb04ac81862907357e09271fe414147b3669509d91d8ffae2ec9c789a5fcd4519629b8f2c7de8d0cce9"),
		Message: &eth2ApiV1Deneb.BlindedBeaconBlock{
			Slot:          1,
			ProposerIndex: 1,
			ParentRoot:    phase0.Root{0x01},
			StateRoot:     phase0.Root{0x02},
			Body: &eth2ApiV1Deneb.BlindedBeaconBlockBody{
				RANDAOReveal: phase0.BLSSignature{0xa1},
				ETH1Data: &phase0.ETH1Data{
					BlockHash: blockHash[:],
				},
				Graffiti: phase0.Hash32{0xa2},
				SyncAggregate: &altair.SyncAggregate{
					SyncCommitteeBits: bitfield.NewBitvector512(),
				},
				ProposerSlashings: []*phase0.ProposerSlashing{},
				AttesterSlashings: []*phase0.AttesterSlashing{},
				Attestations:      []*phase0.Attestation{},
				Deposits:          []*phase0.Deposit{},
				VoluntaryExits:    []*phase0.SignedVoluntaryExit{},
				ExecutionPayloadHeader: &deneb.ExecutionPayloadHeader{
					ParentHash:    mock.HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7"),
					BlockHash:     blockHash,
					BlockNumber:   12345,
					FeeRecipient:  mock.HexToAddress("0xdb65fEd33dc262Fe09D9a2Ba8F80b329BA25f941"),
					BaseFeePerGas: uint256.NewInt(100),
				},
			},
		},
	}

	t.Run("Returns accepted response without payload", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)
		header.Set("Eth-Consensus-Version", "deneb")

		backend := newTestBackend(t, 1, time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusAccepted, rr.Code, rr.Body.String())
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Empty(t, rr.Body.String(), "v2 endpoint should not return payload")
	})

	t.Run("Returns error when no relay responds", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)

		backend := newTestBackend(t, 1, time.Second)
		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusBadGateway, rr.Code)
		require.Equal(t, 0, backend.relays[0].GetRequestCount(path))
	})

	t.Run("Retries on error from relay", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)
		header.Set("Eth-Consensus-Version", "deneb")

		backend := newTestBackend(t, 1, 2*time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		count := 0
		backend.relays[0].OverrideHandleGetPayloadV2(func(w http.ResponseWriter, _ *http.Request) {
			if count > 0 {
				// success response on the second attempt
				backend.relays[0].DefaultHandleGetPayloadV2(w)
			} else {
				w.WriteHeader(http.StatusInternalServerError)
				_, err := w.Write([]byte(`{"code":500,"message":"internal server error"}`))
				require.NoError(t, err, "failed to write error response") //nolint:testifylint // if we fail here the test is compromised
			}
			count++
		})
		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusAccepted, rr.Code, rr.Body.String())
	})

	t.Run("Retries with V1 API when V2 API is not found", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)
		header.Set("Eth-Consensus-Version", "deneb")

		backend := newTestBackend(t, 1, 2*time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		// Override V2 handler to return error, V1 handler to succeed
		backend.relays[0].OverrideHandleGetPayloadV2(func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusNotFound)
			_, err := w.Write([]byte(`{"code":400,"message":"bad request"}`))
			require.NoError(t, err, "failed to write error response") //nolint:testifylint // if we fail here the test is compromised
		})

		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusAccepted, rr.Code, rr.Body.String())

		// Verify both V2 and V1 endpoints were called
		require.Equal(t, 1, backend.relays[0].GetRequestCount(params.PathGetPayloadV2))
		require.Equal(t, 1, backend.relays[0].GetRequestCount(params.PathGetPayload))
	})

	t.Run("Retries with V1 API when V2 API fails", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)
		header.Set("Eth-Consensus-Version", "deneb")

		backend := newTestBackend(t, 1, 2*time.Second)

		// Add the bid to the service
		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		// Override V2 handler to return error, V1 handler to succeed
		backend.relays[0].OverrideHandleGetPayloadV2(func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusInternalServerError)
			_, err := w.Write([]byte(`{"code":500,"message":"internal server error"}`))
			require.NoError(t, err, "failed to write error response") //nolint:testifylint // if we fail here the test is compromised
		})

		rr := backend.request(t, http.MethodPost, path, header, payload)
		require.Equal(t, http.StatusAccepted, rr.Code, rr.Body.String())

		// Verify both V2 and V1 endpoints were called
		require.Equal(t, 1, backend.relays[0].GetRequestCount(params.PathGetPayloadV2))
		require.Equal(t, 1, backend.relays[0].GetRequestCount(params.PathGetPayload))
	})

	t.Run("V2 requests to all relays continue in background after first 202 response", func(t *testing.T) {
		header := make(http.Header)
		header.Set(HeaderAccept, MediaTypeJSON)
		header.Set("Eth-Consensus-Version", "deneb")

		backend := newTestBackend(t, 3, 5*time.Second)

		bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
		for i, relay := range backend.relays {
			bid.relays[i] = relay.RelayEntry
		}
		backend.boost.bids[bidKey(payload.Message.Slot, payload.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

		backend.relays[0].OverrideHandleGetPayloadV2(func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusAccepted)
		})

		delay := 200 * time.Millisecond
		backend.relays[1].OverrideHandleGetPayloadV2(func(w http.ResponseWriter, _ *http.Request) {
			time.Sleep(delay)
			w.WriteHeader(http.StatusAccepted)
		})

		backend.relays[2].OverrideHandleGetPayloadV2(func(w http.ResponseWriter, _ *http.Request) {
			time.Sleep(delay)
			w.WriteHeader(http.StatusAccepted)
		})

		startTime := time.Now()
		rr := backend.request(t, http.MethodPost, path, header, payload)
		firstResponseTime := time.Since(startTime)

		require.Equal(t, http.StatusAccepted, rr.Code, rr.Body.String())
		require.Less(t, firstResponseTime, delay)
		require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
		require.Equal(t, 1, backend.relays[2].GetRequestCount(path))
	})
}

func TestCheckRelays(t *testing.T) {
	t.Run("One relay is okay", func(t *testing.T) {
		backend := newTestBackend(t, 1, time.Second)
		numHealthyRelays := backend.boost.CheckRelays()
		require.Equal(t, 1, numHealthyRelays)
	})

	t.Run("One relay is down", func(t *testing.T) {
		backend := newTestBackend(t, 1, time.Second)
		backend.relays[0].Server.Close()

		numHealthyRelays := backend.boost.CheckRelays()
		require.Equal(t, 0, numHealthyRelays)
	})

	t.Run("One relays is up, one down", func(t *testing.T) {
		backend := newTestBackend(t, 2, time.Second)
		backend.relays[0].Server.Close()

		numHealthyRelays := backend.boost.CheckRelays()
		require.Equal(t, 1, numHealthyRelays)
	})

	t.Run("Should not follow redirects", func(t *testing.T) {
		backend := newTestBackend(t, 1, time.Second)
		redirectAddress := backend.relays[0].Server.URL
		backend.relays[0].Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			http.Redirect(w, r, redirectAddress, http.StatusTemporaryRedirect)
		}))

		url, err := url.ParseRequestURI(backend.relays[0].Server.URL)
		require.NoError(t, err)
		backend.boost.relayConfigs[0].RelayEntry.URL = url
		numHealthyRelays := backend.boost.CheckRelays()
		require.Equal(t, 0, numHealthyRelays)
	})
}

func TestEmptyTxRoot(t *testing.T) {
	transactions := eth2UtilBellatrix.ExecutionPayloadTransactions{Transactions: []bellatrix.Transaction{}}
	txroot, _ := transactions.HashTreeRoot()
	txRootHex := fmt.Sprintf("0x%x", txroot)
	require.Equal(t, "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1", txRootHex)
}

func blindedBlockToBlockResponse(signedBlock any, version spec.DataVersion) *builderApi.VersionedSubmitBlindedBlockResponse {
	switch version {
	case spec.DataVersionBellatrix:
		block, ok := signedBlock.(*eth2ApiV1Bellatrix.SignedBlindedBeaconBlock)
		if !ok {
			panic("failed to convert block")
		}
		header := block.Message.Body.ExecutionPayloadHeader
		return &builderApi.VersionedSubmitBlindedBlockResponse{
			Version: spec.DataVersionBellatrix,
			Bellatrix: &bellatrix.ExecutionPayload{
				ParentHash:    header.ParentHash,
				FeeRecipient:  header.FeeRecipient,
				StateRoot:     header.StateRoot,
				ReceiptsRoot:  header.ReceiptsRoot,
				LogsBloom:     header.LogsBloom,
				PrevRandao:    header.PrevRandao,
				BlockNumber:   header.BlockNumber,
				GasLimit:      header.GasLimit,
				GasUsed:       header.GasUsed,
				Timestamp:     header.Timestamp,
				ExtraData:     header.ExtraData,
				BaseFeePerGas: header.BaseFeePerGas,
				BlockHash:     header.BlockHash,
				Transactions:  make([]bellatrix.Transaction, 0),
			},
		}
	case spec.DataVersionCapella:
		block, ok := signedBlock.(*eth2ApiV1Capella.SignedBlindedBeaconBlock)
		if !ok {
			panic("failed to convert block")
		}
		header := block.Message.Body.ExecutionPayloadHeader
		return &builderApi.VersionedSubmitBlindedBlockResponse{
			Version: spec.DataVersionCapella,
			Capella: &capella.ExecutionPayload{
				ParentHash:    header.ParentHash,
				FeeRecipient:  header.FeeRecipient,
				StateRoot:     header.StateRoot,
				ReceiptsRoot:  header.ReceiptsRoot,
				LogsBloom:     header.LogsBloom,
				PrevRandao:    header.PrevRandao,
				BlockNumber:   header.BlockNumber,
				GasLimit:      header.GasLimit,
				GasUsed:       header.GasUsed,
				Timestamp:     header.Timestamp,
				ExtraData:     header.ExtraData,
				BaseFeePerGas: header.BaseFeePerGas,
				BlockHash:     header.BlockHash,
				Transactions:  make([]bellatrix.Transaction, 0),
				Withdrawals:   make([]*capella.Withdrawal, 0),
			},
		}
	case spec.DataVersionDeneb:
		block, ok := signedBlock.(*eth2ApiV1Deneb.SignedBlindedBeaconBlock)
		if !ok {
			panic("failed to convert block")
		}
		header := block.Message.Body.ExecutionPayloadHeader
		commitments := block.Message.Body.BlobKZGCommitments
		return &builderApi.VersionedSubmitBlindedBlockResponse{
			Version: spec.DataVersionDeneb,
			Deneb:   denebExecutionPayloadAndBlobsBundle(header, commitments),
		}
	case spec.DataVersionElectra:
		block, ok := signedBlock.(*eth2ApiV1Electra.SignedBlindedBeaconBlock)
		if !ok {
			panic("failed to convert block")
		}
		header := block.Message.Body.ExecutionPayloadHeader
		commitments := block.Message.Body.BlobKZGCommitments
		return &builderApi.VersionedSubmitBlindedBlockResponse{
			Version: spec.DataVersionElectra,
			Electra: denebExecutionPayloadAndBlobsBundle(header, commitments),
		}
	case spec.DataVersionFulu:
		block, ok := signedBlock.(*eth2ApiV1Electra.SignedBlindedBeaconBlock)
		if !ok {
			panic("failed to convert block")
		}
		header := block.Message.Body.ExecutionPayloadHeader
		commitments := block.Message.Body.BlobKZGCommitments
		return &builderApi.VersionedSubmitBlindedBlockResponse{
			Version: spec.DataVersionFulu,
			Fulu:    fuluExecutionPayloadAndBlobsBundle(header, commitments),
		}
	case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair:
		panic("unknown data version")
	}
	return nil
}

func denebExecutionPayloadAndBlobsBundle(header *deneb.ExecutionPayloadHeader, kzgCommitments []deneb.KZGCommitment) *builderApiDeneb.ExecutionPayloadAndBlobsBundle {
	numBlobs := len(kzgCommitments)
	commitments := make([]deneb.KZGCommitment, numBlobs)
	copy(commitments, kzgCommitments)
	// For testing, proofs and blobs are not populated
	proofs := make([]deneb.KZGProof, numBlobs)
	blobs := make([]deneb.Blob, numBlobs)

	return &builderApiDeneb.ExecutionPayloadAndBlobsBundle{
		ExecutionPayload: &deneb.ExecutionPayload{
			ParentHash:    header.ParentHash,
			FeeRecipient:  header.FeeRecipient,
			StateRoot:     header.StateRoot,
			ReceiptsRoot:  header.ReceiptsRoot,
			LogsBloom:     header.LogsBloom,
			PrevRandao:    header.PrevRandao,
			BlockNumber:   header.BlockNumber,
			GasLimit:      header.GasLimit,
			GasUsed:       header.GasUsed,
			Timestamp:     header.Timestamp,
			ExtraData:     header.ExtraData,
			BaseFeePerGas: header.BaseFeePerGas,
			BlockHash:     header.BlockHash,
			Transactions:  make([]bellatrix.Transaction, 0),
			Withdrawals:   make([]*capella.Withdrawal, 0),
			BlobGasUsed:   header.BlobGasUsed,
			ExcessBlobGas: header.ExcessBlobGas,
		},
		BlobsBundle: &builderApiDeneb.BlobsBundle{
			Commitments: commitments,
			Proofs:      proofs,
			Blobs:       blobs,
		},
	}
}

func fuluExecutionPayloadAndBlobsBundle(header *deneb.ExecutionPayloadHeader, kzgCommitments []deneb.KZGCommitment) *builderApiFulu.ExecutionPayloadAndBlobsBundle {
	numBlobs := len(kzgCommitments)
	commitments := make([]deneb.KZGCommitment, numBlobs)
	copy(commitments, kzgCommitments)
	// For testing, proofs and blobs are not populated
	proofs := make([]deneb.KZGProof, 0, numBlobs*common.CellsPerExtBlob)
	blobs := make([]deneb.Blob, numBlobs)

	return &builderApiFulu.ExecutionPayloadAndBlobsBundle{
		ExecutionPayload: &deneb.ExecutionPayload{
			ParentHash:    header.ParentHash,
			FeeRecipient:  header.FeeRecipient,
			StateRoot:     header.StateRoot,
			ReceiptsRoot:  header.ReceiptsRoot,
			LogsBloom:     header.LogsBloom,
			PrevRandao:    header.PrevRandao,
			BlockNumber:   header.BlockNumber,
			GasLimit:      header.GasLimit,
			GasUsed:       header.GasUsed,
			Timestamp:     header.Timestamp,
			ExtraData:     header.ExtraData,
			BaseFeePerGas: header.BaseFeePerGas,
			BlockHash:     header.BlockHash,
			Transactions:  make([]bellatrix.Transaction, 0),
			Withdrawals:   make([]*capella.Withdrawal, 0),
			BlobGasUsed:   header.BlobGasUsed,
			ExcessBlobGas: header.ExcessBlobGas,
		},
		BlobsBundle: &builderApiFulu.BlobsBundle{
			Commitments: commitments,
			Proofs:      proofs,
			Blobs:       blobs,
		},
	}
}

func TestGetPayloadForks(t *testing.T) {
	t.Parallel()

	// Get a list of testdata files
	pattern := "../testdata/signed-blinded-beacon-block-*.json"
	files, err := filepath.Glob(pattern)
	require.NoError(t, err)
	require.NotEmpty(t, files)

	for _, file := range files {
		file := file // capture variable for closure
		t.Run(fmt.Sprintf("File=%s", file), func(t *testing.T) {
			t.Parallel()

			// Read the test JSON from file
			jsonBytes, err := os.ReadFile(file)
			require.NoError(t, err)

			re := regexp.MustCompile(`^.*signed-blinded-beacon-block-(.+)\.json$`)
			matches := re.FindStringSubmatch(file)
			require.Len(t, matches, 2, "filename did not match expected pattern")
			consensusVersion := matches[1]

			header := http.Header{}
			header.Set("Accept", "application/json")
			header.Set("Content-Type", "application/json")
			header.Set("Eth-Consensus-Version", consensusVersion)

			// Create a new backend
			backend := newTestBackend(t, 1, time.Second)

			// Decode the block
			block := new(eth2Api.VersionedSignedBlindedBeaconBlock)
			err = decodeSignedBlindedBeaconBlock(jsonBytes, MediaTypeJSON, consensusVersion, block)
			require.NoError(t, err)

			// Get the request slot and block hash
			slot, err := block.Slot()
			require.NoError(t, err)
			blockHash, err := block.ExecutionBlockHash()
			require.NoError(t, err)

			// Set up the bid in the backend
			bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
			for i, r := range backend.relays {
				bid.relays[i] = r.RelayEntry
			}
			backend.boost.bids[bidKey(slot, blockHash)] = bid

			// Choose the payload based on the block version
			var payload interface{}
			switch block.Version {
			case spec.DataVersionBellatrix:
				payload = block.Bellatrix
			case spec.DataVersionCapella:
				payload = block.Capella
			case spec.DataVersionDeneb:
				payload = block.Deneb
			case spec.DataVersionElectra:
				payload = block.Electra
			case spec.DataVersionFulu:
				payload = block.Fulu
			case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair:
				require.Fail(t, "unsupported version")
			}

			// Configure the relay's expected response and send the request
			backend.relays[0].GetPayloadResponse = blindedBlockToBlockResponse(payload, block.Version)
			rr := backend.request(t, http.MethodPost, params.PathGetPayload, header, payload)

			// Validate the response
			require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
			require.Equal(t, 1, backend.relays[0].GetRequestCount(params.PathGetPayload))
			resp := new(builderApi.VersionedSubmitBlindedBlockResponse)
			require.NoError(t, json.Unmarshal(rr.Body.Bytes(), resp))
			respBlockHash, err := resp.BlockHash()
			require.NoError(t, err)
			require.Equal(t, blockHash, respBlockHash)
		})
	}
}

func TestGetPayloadToAllRelays(t *testing.T) {
	header := make(http.Header)
	header.Set(HeaderAccept, MediaTypeJSON)
	header.Set("Eth-Consensus-Version", "deneb")

	// Load the signed blinded beacon block used for getPayload
	jsonFile, err := os.Open("../testdata/signed-blinded-beacon-block-deneb.json")
	require.NoError(t, err)
	defer jsonFile.Close()
	signedBlindedBeaconBlock := new(eth2ApiV1Deneb.SignedBlindedBeaconBlock)
	require.NoError(t, DecodeJSON(jsonFile, &signedBlindedBeaconBlock))

	// Create a test backend with 2 relays
	backend := newTestBackend(t, 2, time.Second)

	// overriding genesisTime so slot 12345 appears to be now
	backend.boost.genesisTime = uint64(time.Now().Unix()) - 12345*12

	// Add the bid to the service
	bid := bidResp{relays: make([]types.RelayEntry, len(backend.relays))}
	for i, relay := range backend.relays {
		bid.relays[i] = relay.RelayEntry
	}
	backend.boost.bids[bidKey(signedBlindedBeaconBlock.Message.Slot, signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader.BlockHash)] = bid

	// call getHeader, highest bid is returned by relay 0
	getHeaderPath := "/eth/v1/builder/header/12345/0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2/0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249"
	backend.relays[0].GetHeaderResponse = backend.relays[0].MakeGetHeaderResponse(
		12345,
		"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
		"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
		"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
		spec.DataVersionDeneb,
	)
	rr := backend.request(t, http.MethodGet, getHeaderPath, header, nil)
	require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
	require.Equal(t, 1, backend.relays[0].GetRequestCount(getHeaderPath))
	require.Equal(t, 1, backend.relays[1].GetRequestCount(getHeaderPath))

	// Prepare getPayload response
	backend.relays[0].GetPayloadResponse = blindedBlockToBlockResponse(signedBlindedBeaconBlock, spec.DataVersionDeneb)

	// call getPayload, ensure it's called to all relays
	rr = backend.request(t, http.MethodPost, params.PathGetPayload, header, signedBlindedBeaconBlock)
	require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
	require.Equal(t, 1, backend.relays[0].GetRequestCount(params.PathGetPayload))
	require.Equal(t, 1, backend.relays[1].GetRequestCount(params.PathGetPayload))
}
